/*-------------------------------------------------------------------------
 *
 * catalog.c
 *		涉及目录命名约定和其他硬编码知识的常规例程
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/catalog/catalog.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <fcntl.h>
#include <unistd.h>

#include "access/genam.h"
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/table.h"
#include "access/transam.h"
#include "catalog/catalog.h"
#include "catalog/namespace.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
#ifdef FDD
#include "catalog/fdb_mac_auth.h"
#endif
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_largeobject.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_parameter_acl.h"
#include "catalog/pg_replication_origin.h"
#include "catalog/pg_shdepend.h"
#include "catalog/pg_shdescription.h"
#include "catalog/pg_shseclabel.h"
#include "catalog/pg_subscription.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_type.h"
#include "miscadmin.h"
#include "storage/fd.h"
#include "utils/fmgroids.h"
#include "utils/fmgrprotos.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"

/*
 * 参数确定何时在
 * GetNewOidWithIndex()中发出日志消息
 */
#define GETNEWOID_LOG_THRESHOLD 1000000
#define GETNEWOID_LOG_MAX_INTERVAL 128000000

/*
 * IsSystemRelation
 *		仅在关系是系统目录或toast表时为真。
 *		有关系统目录的确切定义，请参见IsCatalogRelation。
 *
 *		为保护起见，我们将用户关系的toast表视为“系统关系”，
 *		例如，您不能在没有特殊权限的情况下更改其架构。
 *		因此，这个函数大多数用途是检查allow_system_table_mods限制是否适用。
 *		出于其他目的，请考虑是否应使用
 *		IsCatalogRelation。
 *
 *		此函数不执行任何目录访问。
 *		一些调用者依赖这一点！
 */
bool IsSystemRelation(Relation fc_relation)
{
	return IsSystemClass(RelationGetRelid(fc_relation), fc_relation->rd_rel);
}

/*
 * IsSystemClass
 *		与上述类似，但以Form_pg_class作为参数。
 *		当我们不想打开关系并需要直接搜索pg_class时使用。
 */
bool IsSystemClass(Oid fc_relid, Form_pg_class fc_reltuple)
{
	/* IsCatalogRelationOid稍快，因此首先测试该函数 */
	return (IsCatalogRelationOid(fc_relid) || IsToastClass(fc_reltuple));
}

/*
 * IsCatalogRelation
 *		仅在关系是系统目录时为真。
 *
 *		我们所说的系统目录是指在initdb的引导阶段创建的目录。
 *		这不仅包括目录本身，还包括它们的索引，以及TOAST表和索引（如果有）。
 *
 *		此函数不执行任何目录访问。
 *		一些调用者依赖这一点！
 */
bool IsCatalogRelation(Relation fc_relation)
{
	return IsCatalogRelationOid(RelationGetRelid(fc_relation));
}

/*
 * IsCatalogRelationOid
 *		仅在由此OID标识的关系是系统目录时为真。
 *
 *		我们所说的系统目录是指在initdb的引导阶段创建的目录。
 *		这不仅包括目录本身，还包括它们的索引，以及TOAST表和索引（如果有）。
 *
 *		此函数不执行任何目录访问。
 *		一些调用者依赖这一点！
 */
bool IsCatalogRelationOid(Oid fc_relid)
{
	/*
	 * 如果关系具有固定的OID，我们认为它是系统目录。
	 * 这包括所有定义的目录、它们的索引，以及它们的TOAST
	 * 表和索引。
	 *
	 * 这一规则排除了information_schema中的关系，因为它们不是
	 * 系统的组成部分，可以像用户关系一样处理。
	 * （由于可以有效地删除和重建information_schema，任何没有
	 * 这样操作的规则都是错误的。）
	 *
	 * 由于OID回绕将跳过此范围的
	 * OID，因此此测试是可靠的；请参见GetNewObjectId()。
	 */
	return (fc_relid < (Oid) FirstUnpinnedObjectId);
}

/*
 * IsInplaceUpdateRelation
 *		仅当核心代码对该关系执行原地更新时为真。
 *
 *		这用于断言，并使执行器遵循在README.tuplock部分“锁定以写入
 *		原地更新表”中描述的锁定协议。扩展可能会原地更新其他堆
 *		表，但是在同一表上的并发SQL UPDATE可能会覆盖
 *		这些修改。
 *
 *		执行器可以假设这些不是分区或分区表，并且没有触发器。
 */
bool IsInplaceUpdateRelation(Relation fc_relation)
{
	return IsInplaceUpdateOid(RelationGetRelid(fc_relation));
}

/*
 * IsInplaceUpdateOid
 *		与上述类似，但以OID作为参数。
 */
bool IsInplaceUpdateOid(Oid fc_relid)
{
	return (fc_relid == RelationRelationId ||
			fc_relid == DatabaseRelationId);
}

/*
 * IsToastRelation
 *		仅在关系是TOAST支持关系（或索引）时为真。
 *
 *		不执行任何目录访问。
 */
bool IsToastRelation(Relation fc_relation)
{
	/*
	 * 我们实际检查的是关系是否属于pg_toast
	 * 命名空间。由于在其他地方对在pg_toast命名空间中创建用户关系或
	 * 在该命名空间中/外移动关系的限制，这应该是等价的。
	 * 还要注意，这不会对其他会话的临时表中的toast表返回“真”；
	 * 我们期望其他机制会防止对这些的访问。
	 */
	return IsToastNamespace(RelationGetNamespace(fc_relation));
}

/*
 * IsToastClass
 *		与上述类似，但以Form_pg_class作为参数。
 *		当我们不想打开关系并需要直接搜索pg_class时使用。
 */
bool IsToastClass(Form_pg_class fc_reltuple)
{
	Oid			fc_relnamespace = fc_reltuple->relnamespace;

	return IsToastNamespace(fc_relnamespace);
}

/*
 * IsCatalogNamespace
 *		仅在命名空间是pg_catalog时为真。
 *
 *		不执行任何目录访问。
 *
 * 注意：这不是宏的原因是为了避免在许多地方包含
 * catalog/pg_namespace.h。
 */
bool IsCatalogNamespace(Oid fc_namespaceId)
{
	return fc_namespaceId == PG_CATALOG_NAMESPACE;
}


/*
 * IsToastNamespace
 *		当且仅当命名空间是 pg_toast 或我的临时吐司表命名空间时返回真。
 *
 *		不执行任何目录访问。
 *
 * 注意：对于属于其他后端的临时吐司表命名空间，这将返回假。那些被视为与其他后端的常规临时表命名空间相同，并在适当的地方阻止访问。
 * 如果您需要检查那些，您可以使用 isAnyTempNamespace，但请注意，这确实涉及目录访问。
 */
bool IsToastNamespace(Oid fc_namespaceId)
{
	return (fc_namespaceId == PG_TOAST_NAMESPACE) ||
		isTempToastNamespace(fc_namespaceId);
}


/*
 * IsReservedName
 *		当且仅当名称以 pg_ 前缀开头时返回真。
 *
 *		对于某些类的对象，前缀 pg_ 仅保留给系统对象。 从 8.0 开始，这仅适用于模式和表空间名称。 从 9.6 开始，这对于角色也适用。
 */
bool IsReservedName(const char *fc_name)
{
	/* 丑陋的编码以提高速度 */
	return (fc_name[0] == 'p' &&
			fc_name[1] == 'g' &&
			fc_name[2] == '_');
}


/*
 * IsSharedRelation
 *		给定关系的 OID，确定它是否应该在整个数据库集群中共享。
 *
 * 在较早版本中，这必须硬编码，以便我们能够计算关系的锁定标记并在检查其目录条目之前对其进行锁定。
 * 由于我们现在拥有 MVCC 目录访问，因此使这一硬性要求的竞争条件已经消失，因此我们可以考虑放宽这一限制。
 * 然而，如果我们扫描 pg_class 条目以查找 relisshared，然后再锁定关系，pg_class 可能在此期间被更新，
 * 强迫我们再次扫描关系，这肯定会复杂，并可能产生不良的性能后果。 幸运的是，共享关系的集合相对静态，因此手动维护它们的 OID 列表并不是完全不切实际的。
 */
bool IsSharedRelation(Oid fc_relationId)
{
	/* 这些是共享目录（查找 BKI_SHARED_RELATION） */
	if (fc_relationId == AuthIdRelationId ||
		fc_relationId == AuthMemRelationId ||
		fc_relationId == DatabaseRelationId ||
		fc_relationId == DbRoleSettingRelationId ||
		fc_relationId == ParameterAclRelationId ||
		fc_relationId == ReplicationOriginRelationId ||
		fc_relationId == SharedDependRelationId ||
		fc_relationId == SharedDescriptionRelationId ||
		fc_relationId == SharedSecLabelRelationId ||
		fc_relationId == SubscriptionRelationId ||
#ifdef FDD //将表fdb_mac_auth增加为系统共享表
		fc_relationId == FdbMacAuthRelationId ||
#endif
		fc_relationId == TableSpaceRelationId)
		return true;
	/* 这些是它们的索引 */
	if (fc_relationId == AuthIdOidIndexId ||
		fc_relationId == AuthIdRolnameIndexId ||
		fc_relationId == AuthMemMemRoleIndexId ||
		fc_relationId == AuthMemRoleMemIndexId ||
		fc_relationId == DatabaseNameIndexId ||
		fc_relationId == DatabaseOidIndexId ||
		fc_relationId == DbRoleSettingDatidRolidIndexId ||
		fc_relationId == ParameterAclOidIndexId ||
		fc_relationId == ParameterAclParnameIndexId ||
		fc_relationId == ReplicationOriginIdentIndex ||
		fc_relationId == ReplicationOriginNameIndex ||
		fc_relationId == SharedDependDependerIndexId ||
		fc_relationId == SharedDependReferenceIndexId ||
		fc_relationId == SharedDescriptionObjIndexId ||
		fc_relationId == SharedSecLabelObjectIndexId ||
		fc_relationId == SubscriptionNameIndexId ||
		fc_relationId == SubscriptionObjectIndexId ||
		fc_relationId == TablespaceNameIndexId ||
		fc_relationId == TablespaceOidIndexId)
		return true;
	/* 这些是它们的吐司表和吐司索引 */
	if (fc_relationId == PgAuthidToastTable ||
		fc_relationId == PgAuthidToastIndex ||
		fc_relationId == PgDatabaseToastTable ||
		fc_relationId == PgDatabaseToastIndex ||
		fc_relationId == PgDbRoleSettingToastTable ||
		fc_relationId == PgDbRoleSettingToastIndex ||
		fc_relationId == PgParameterAclToastTable ||
		fc_relationId == PgParameterAclToastIndex ||
		fc_relationId == PgReplicationOriginToastTable ||
		fc_relationId == PgReplicationOriginToastIndex ||
		fc_relationId == PgShdescriptionToastTable ||
		fc_relationId == PgShdescriptionToastIndex ||
		fc_relationId == PgShseclabelToastTable ||
		fc_relationId == PgShseclabelToastIndex ||
		fc_relationId == PgSubscriptionToastTable ||
		fc_relationId == PgSubscriptionToastIndex ||
		fc_relationId == PgTablespaceToastTable ||
		fc_relationId == PgTablespaceToastIndex)
		return true;
	return false;
}

/*
 * IsPinnedObject
 *		给定数据库对象的类 + OID 身份，报告它是否是“固定”的，即由于系统的要求不能被删除。
 *
 * 我们曾在 pg_depend 中明确表示这一点，但这被证明是不可取的开销，因此现在我们依赖 OID 范围测试。
 */
bool IsPinnedObject(Oid fc_classId, Oid fc_objectId)
{
	/*
	 * OID 大于 FirstUnpinnedObjectId 的对象从不被固定。由于
	 * OID 生成器在循环时会跳过这个范围，因此此检查
	 * 确保用户定义的对象不会被视为固定。
	 */
	if (fc_objectId >= FirstUnpinnedObjectId)
		return false;

	/*
	 * 大对象从不被固定。我们需要这个特殊情况，因为
	 * 它们的 OID 可能是用户指定的。
	 */
	if (fc_classId == LargeObjectRelationId)
		return false;

	/*
	 * 在目录 .dat 文件中定义了一些对象，出于政策原因，
	 * 我们更倾向于不将其视为固定。我们以前通过将它们排除在 pg_depend 之外来处理这个问题，
	 * 但在这里硬编码它们的 OID 同样简单。 （如果用户确实删除并重新创建
	 * 它们，它们会有新的但肯定未固定的 OID，所以没问题。）
	 *
	 * 检查 classId 和 objectId 是多余的，因为 OID 小于
	 * FirstGenbkiObjectId 的应该是全局唯一的，但为了
	 * 稳健性还是这样做。
	 */

	/* 公共命名空间未被固定 */
	if (fc_classId == NamespaceRelationId &&
		fc_objectId == PG_PUBLIC_NAMESPACE)
		return false;

	/*
	 * 数据库从不被固定。虽然看起来固定至少 template0 是谨慎的；
	 * 但我们故意这么做，以便可以相互重建 template0 和
	 * template1，从而让它们作为相互备份（只要您没有修改 template1）。
	 */
	if (fc_classId == DatabaseRelationId)
		return false;

	/*
	 * 所有其他 initdb 创建的对象都是固定的。这是多余的（系统
	 * 实际上并不依赖于每个奇怪的数据类型，例如），但仅生成最小所需的依赖集
	 * 似乎很难，而强制执行准确的列表将比这里使用的简单范围测试
	 * 要昂贵得多。
	 */
	return true;
}


/*
 * GetNewOidWithIndex
 *		生成一个在系统关系中唯一的新 OID。
 *
 * 由于 OID 不会立即插入到表中，这里存在一个竞争条件；
 * 但只有在其他人成功循环通过 2^32 OID 并在我们完成插入之前
 * 生成相同的 OID 时才会出现问题。这似乎不太可能是问题。
 * 注意，如果我们必须 *提交* 行以结束竞争条件，风险会相对较高；
 * 因此我们在测试中使用 SnapshotAny，这样
 * 我们将看到未提交的行。 （我们曾经使用 SnapshotDirty，但这样做的缺点是
 * 它会忽略最近删除的行，导致只要我们自己的 MVCC 快照认为最近删除的行是活动的，
 * 就会创建瞬态冲突的风险。选择 TOAST 
 * OID 时风险更高，因为 SnapshotToast 将死行视为无限活动。）
 *
 * 注意，我们实际上假设表中有相对较少的条目（远少于 2^32），
 * 并且没有非常长的连续存在的 OID。对于
 * 系统目录来说，这是一个大致合理的假设。
 *
 * 调用者必须对关系拥有合适的锁。
 */
Oid GetNewOidWithIndex(Relation fc_relation, Oid fc_indexId, AttrNumber fc_oidcolumn)
{
	Oid			fc_newOid;
	SysScanDesc fc_scan;
	ScanKeyData fc_key;
	bool		fc_collides;
	uint64		fc_retries = 0;
	uint64		fc_retries_before_log = GETNEWOID_LOG_THRESHOLD;

	/* 仅支持系统关系 */
	Assert(IsSystemRelation(fc_relation));

	/* 在引导模式下，我们没有任何可以使用的索引 */
	if (IsBootstrapProcessingMode())
		return GetNewObjectId();

	/*
	 * 在 pg_upgrade 期间，我们绝不会被要求生成新的 pg_type OID；
	 * 这样做会有与它想分配的 OID 碰撞的风险。触发此断言意味着存在某种路径，
	 * 我们未能确保类型 OID 是由转储脚本中的命令确定的。
	 */
	Assert(!IsBinaryUpgrade || RelationGetRelid(fc_relation) != TypeRelationId);

	/* 生成新 OID，直到找到一个在表中未使用的 */
	do
	{
		CHECK_FOR_INTERRUPTS();

		fc_newOid = GetNewObjectId();

		ScanKeyInit(&fc_key,
					fc_oidcolumn,
					BTEqualStrategyNumber, F_OIDEQ,
					ObjectIdGetDatum(fc_newOid));

		/* 请参阅上述关于使用 SnapshotAny 的说明 */
		fc_scan = systable_beginscan(fc_relation, fc_indexId, true,
								  SnapshotAny, 1, &fc_key);

		fc_collides = HeapTupleIsValid(systable_getnext(fc_scan));

		systable_endscan(fc_scan);

		/*
		 * 记录我们迭代超过 GETNEWOID_LOG_THRESHOLD 但尚未
		 * 找到在关系中未使用的 OID。然后以指数增加的间隔重复记录，
		 * 直到我们迭代超过 GETNEWOID_LOG_MAX_INTERVAL。最后，
		 * 每个 GETNEWOID_LOG_MAX_INTERVAL 重复记录，除非找到未使用的 OID。
		 * 这段逻辑是为了不让服务器日志充满类似的信息。
		 */
		if (fc_retries >= fc_retries_before_log)
		{
			ereport(LOG,
					(errmsg("still searching for an unused OID in relation \"%s\"",
							RelationGetRelationName(fc_relation)),
					 errdetail_plural("OID candidates have been checked %llu time, but no unused OID has been found yet.",
									  "OID candidates have been checked %llu times, but no unused OID has been found yet.",
									  fc_retries,
									  (unsigned long long) fc_retries)));

			/*
			 * 将下一个日志记录之前要做的重试次数加倍，
			 * 直到达到 GETNEWOID_LOG_MAX_INTERVAL。
			 */
			if (fc_retries_before_log * 2 <= GETNEWOID_LOG_MAX_INTERVAL)
				fc_retries_before_log *= 2;
			else
				fc_retries_before_log += GETNEWOID_LOG_MAX_INTERVAL;
		}

		fc_retries++;
	} while (fc_collides);

	/*
	 * 如果至少发出一条日志消息，还记录 OID 分配的完成情况。
	 */
	if (fc_retries > GETNEWOID_LOG_THRESHOLD)
	{
		ereport(LOG,
				(errmsg_plural("new OID has been assigned in relation \"%s\" after %llu retry",
							   "new OID has been assigned in relation \"%s\" after %llu retries",
							   fc_retries,
							   RelationGetRelationName(fc_relation), (unsigned long long) fc_retries)));
	}

	return fc_newOid;
}

/*
 * GetNewRelFileNode
 *		生成一个在给定表空间的数据库中唯一的新 relfilenode 编号。
 *
 * 如果 relfilenode 也将用作关系的 OID，请传递已打开的 pg_class 目录，
 * 并且此例程将保证结果也是 pg_class 中未使用的 OID。
 * 如果结果仅用作现有关系的 relfilenode，则传递 NULL 作为 pg_class。
 *
 * 与 GetNewOidWithIndex() 一样，理论上存在一个竞争条件的风险，
 * 但似乎不值得担心。
 *
 * 注意：我们不支持在引导模式下使用此功能。引导创建的所有关系
 * 都有预先分配的 OID，因此没有必要。
 */
Oid GetNewRelFileNode(Oid fc_reltablespace, Relation pg_class, char fc_relpersistence)
{
	RelFileNodeBackend fc_rnode;
	char	   *fc_rpath;
	bool		fc_collides;
	BackendId	fc_backend;

	
/*
	 * 如果我们在 pg_upgrade 过程中到达这里，那将是有问题的；在二进制升级运行期间，所有的 relfilenode 分配应该由转储脚本中的命令决定。
	 */
	Assert(!IsBinaryUpgrade);

	switch (fc_relpersistence)
	{
		case RELPERSISTENCE_TEMP:
			fc_backend = BackendIdForTempRelations();
			break;
		case RELPERSISTENCE_UNLOGGED:
		case RELPERSISTENCE_PERMANENT:
			fc_backend = InvalidBackendId;
			break;
		default:
			elog(ERROR, "invalid relpersistence: %c", fc_relpersistence);
			return InvalidOid;	/* 安抚编译器 */
	}

	/* 这个逻辑应该与 RelationInitPhysicalAddr 匹配 */
	fc_rnode.node.spcNode = fc_reltablespace ? fc_reltablespace : MyDatabaseTableSpace;
	fc_rnode.node.dbNode = (fc_rnode.node.spcNode == GLOBALTABLESPACE_OID) ? InvalidOid : MyDatabaseId;

	/*
	 * relpath 将根据后端 ID 的不同而变化，因此我们必须在这里正确初始化，以确保基于文件名的任何冲突都能被正确检测到。
	 */
	fc_rnode.backend = fc_backend;

	do
	{
		CHECK_FOR_INTERRUPTS();

		/* 生成 OID */
		if (pg_class)
			fc_rnode.node.relNode = GetNewOidWithIndex(pg_class, ClassOidIndexId,
													Anum_pg_class_oid);
		else
			fc_rnode.node.relNode = GetNewObjectId();

		/* 检查同名文件是否存在 */
		fc_rpath = relpath(fc_rnode, MAIN_FORKNUM);

		if (access(fc_rpath, F_OK) == 0)
		{
			/* 明确的冲突 */
			fc_collides = true;
		}
		else
		{
			/*
			 * 在这里我们遇到了一点困境：如果 errno 是其他值而不是 ENOENT，我们应该声明冲突并循环吗？实际上，似乎无论 errno 如何，都最好继续进行。如果存在冲突文件，当我们尝试创建新的关系文件时会得到一个 smgr 失败。
			 */
			fc_collides = false;
		}

		pfree(fc_rpath);
	} while (fc_collides);

	return fc_rnode.node.relNode;
}

/*
 * GetNewOidWithIndex() 的 SQL 可调用接口。除了 initdb 直接插入到目录表中，以及从损坏中恢复外，这通常不需要。
 *
 * 此函数故意在用户文档中未进行记录。
 */
Datum pg_nextoid(PG_FUNCTION_ARGS)
{
	Oid			fc_reloid = PG_GETARG_OID(0);
	Name		fc_attname = PG_GETARG_NAME(1);
	Oid			fc_idxoid = PG_GETARG_OID(2);
	Relation	fc_rel;
	Relation	fc_idx;
	HeapTuple	fc_atttuple;
	Form_pg_attribute fc_attform;
	AttrNumber	fc_attno;
	Oid			fc_newoid;

	/*
	 * 由于此函数并不打算在正常运行期间使用，并且仅支持系统目录（修改这些目录需要超级用户权限），因此仅检查超级用户应该不会妨碍有效的用例。
	 */
	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to call %s()",
						"pg_nextoid")));

	fc_rel = table_open(fc_reloid, RowExclusiveLock);
	fc_idx = index_open(fc_idxoid, RowExclusiveLock);

	if (!IsSystemRelation(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("pg_nextoid() can only be used on system catalogs")));

	if (fc_idx->rd_index->indrelid != RelationGetRelid(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("index \"%s\" does not belong to table \"%s\"",
						RelationGetRelationName(fc_idx),
						RelationGetRelationName(fc_rel))));

	fc_atttuple = SearchSysCacheAttName(fc_reloid, NameStr(*fc_attname));
	if (!HeapTupleIsValid(fc_atttuple))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_COLUMN),
				 errmsg("column \"%s\" of relation \"%s\" does not exist",
						NameStr(*fc_attname), RelationGetRelationName(fc_rel))));

	fc_attform = ((Form_pg_attribute) GETSTRUCT(fc_atttuple));
	fc_attno = fc_attform->attnum;

	if (fc_attform->atttypid != OIDOID)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("column \"%s\" is not of type oid",
						NameStr(*fc_attname))));

	if (IndexRelationGetNumberOfKeyAttributes(fc_idx) != 1 ||
		fc_idx->rd_index->indkey.values[0] != fc_attno)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("index \"%s\" is not the index for column \"%s\"",
						RelationGetRelationName(fc_idx),
						NameStr(*fc_attname))));

	fc_newoid = GetNewOidWithIndex(fc_rel, fc_idxoid, fc_attno);

	ReleaseSysCache(fc_atttuple);
	table_close(fc_rel, RowExclusiveLock);
	index_close(fc_idx, RowExclusiveLock);

	PG_RETURN_OID(fc_newoid);
}

/*
 * StopGeneratingPinnedObjectIds() 的 SQL 可调用接口。
 *
 * 这仅用于 initdb，因此故意在用户文档中未进行记录。
 */
Datum pg_stop_making_pinned_objects(PG_FUNCTION_ARGS)
{
	/*
	 * 带保险的检查，因为 StopGeneratingPinnedObjectIds 在非单用户模式下无论如何都会失败。
	 */
	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to call %s()",
						"pg_stop_making_pinned_objects")));

	StopGeneratingPinnedObjectIds();

	PG_RETURN_VOID();
}
